Skip to content

Fix GH-21682: Add NOT_SERIALIZABLE to ZipArchive, XMLWriter, XMLReader, SNMP, tidy, tidyNode#21694

Open
iliaal wants to merge 2 commits intophp:masterfrom
iliaal:fix/gh-21682-ziparchive-not-serializable
Open

Fix GH-21682: Add NOT_SERIALIZABLE to ZipArchive, XMLWriter, XMLReader, SNMP, tidy, tidyNode#21694
iliaal wants to merge 2 commits intophp:masterfrom
iliaal:fix/gh-21682-ziparchive-not-serializable

Conversation

@iliaal
Copy link
Copy Markdown
Contributor

@iliaal iliaal commented Apr 9, 2026

Part of #21682

Five classes wrap native C handles but allow serialization. Unserializing them produces objects with NULL internal pointers.

Segfault:

  • tidyNode (ext/tidy): crashes on hasChildren(), dangling TidyNode pointer

Silent data loss:

  • tidy (ext/tidy): body() returns NULL, cleanRepair() no-ops
  • SNMP (ext/snmp): methods run against a dead session without error

Throws on use:

  • XMLWriter (ext/xmlwriter): methods throw "Invalid or uninitialized XMLWriter object"
  • XMLReader (ext/xmlreader): read() throws "Data must be loaded before reading"

Adds @not-serializable to all five so serialize() throws instead of producing broken objects.

The UAF exploit test bug72479.phpt instantiated SNMP via unserialize(). With NOT_SERIALIZABLE, unserialize() rejects the class and closes the UAF vector.

ZipArchive moved to a follow-up PR after review feedback.

@ndossche
Copy link
Copy Markdown
Member

ndossche commented Apr 9, 2026

One thing to keep in mind is that it may introduce issues analogous to #8996
Perhaps the not-serializable flag is not the right way to do it, I was never really convinced that also blocking children is the right thing to do.

@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Apr 9, 2026

Serializing these classes today: tidyNode segfaults on hasChildren(), ZipArchive/tidy silently lose state, XMLWriter/XMLReader throw on every method call. None produce a usable object.

Re child classes and GH-8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state. DOMDocument has saveXML()/loadXML(), which makes SerializableDomDocument viable. These classes have no equivalent. The underlying handles (xmlTextWriterPtr, TidyDoc, libzip archive, SNMP session) are opaque. A child adding __serialize would track its own state and rebuild from scratch on wakeup, not serialize the parent. The GH-8996 pattern doesn't apply here in practice.

Happy to switch to the DOM-style custom handler if you'd prefer that over NOT_SERIALIZABLE, I do think though it introduces complexity for little meaningful value though.

Copy link
Copy Markdown
Member

@ndossche ndossche left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Fair enough, see my nitpick remark.

@@ -1,29 +1,17 @@
--TEST--
Bug #72434: ZipArchive class Use After Free Vulnerability in PHP's GC algorithm and unserialize
--EXTENSIONS--
Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This test should be moved to ext/zip/tests

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

fair enough, moved as asked.

@iliaal iliaal requested a review from ndossche April 9, 2026 20:50
Copy link
Copy Markdown
Member

@DanielEScherzer DanielEScherzer left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Re child classes and #8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state

ZipArchive will have a way to capture this soon with #21497, see https://github.com/tstarling/php-src/blob/de15d91b44fe56d7e02c4ada8e6e7c0ea13ef146/ext/zip/tests/ZipArchive_closeString_basic.phpt, so allowing subclasses to be serialized might be useful - can you split out the ZipArchive changes? In the absence of a maintainer for ext/zip we should at least give more consideration after #21497 before deciding to prevent subclass serialization

nicolas-grekas added a commit to symfony/polyfill that referenced this pull request Apr 10, 2026
…ND_TRIPPABLE

These classes wrap native C handles and silently lose state through
property restoration. Matches php/php-src#21694 which adds
NOT_SERIALIZABLE to them; the explicit list protects older PHP
versions where the flag isn't set yet.
@iliaal
Copy link
Copy Markdown
Contributor Author

iliaal commented Apr 10, 2026

Re child classes and #8996: only tidyNode is final, but unlike DOM none of these classes can capture or restore their internal C state

ZipArchive will have a way to capture this soon with #21497, see https://github.com/tstarling/php-src/blob/de15d91b44fe56d7e02c4ada8e6e7c0ea13ef146/ext/zip/tests/ZipArchive_closeString_basic.phpt, so allowing subclasses to be serialized might be useful - can you split out the ZipArchive changes? In the absence of a maintainer for ext/zip we should at least give more consideration after #21497 before deciding to prevent subclass serialization

Granted, closeString() makes subclass serialization workable. My reservation is narrower: the common case is the archive bytes, and openString()/closeString() already round-trip those without a wrapper. A subclass only helps when it carries extra state alongside the archive, which is a narrow niche. That said, I take the point that blocking it now forecloses a path that might matter later.

I'll split the ZipArchive change into its own PR so the other changes can go in. We can revisit Zip once #21497 merges and we see what subclasses actually need.

iliaal added 2 commits April 10, 2026 06:47
These classes wrap native C handles (libxml2 writer/reader, SNMP
session, libTidy document/node) that cannot survive serialization.
Unserializing produces a broken object with NULL internal pointers.
bug72479.phpt tested a UAF via unserialize() of SNMP. With
NOT_SERIALIZABLE, unserialize() rejects the class entirely, preventing
the UAF by construction. Update the test to verify the rejection.
@iliaal iliaal force-pushed the fix/gh-21682-ziparchive-not-serializable branch from 602c0b0 to 119e9dd Compare April 10, 2026 10:57
@iliaal iliaal requested a review from DanielEScherzer April 10, 2026 11:00
@DanielEScherzer DanielEScherzer dismissed their stale review April 10, 2026 15:31

PR title needs to be updated, but otherwise no remaining objections about ZipArchive

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants